﻿//-----------------------------------------------------------------------
// <copyright file="TaskQueue.cs" 
//        company="MicroMan6502">
//
//    Copyright (c)2010 MicroMan6502. All rights reserved.
//
// </copyright>
//-----------------------------------------------------------------------

namespace EasyThreading
{
    using System;
    using System.Threading;

    /// <summary>
    /// Defines a class used to queue tasks. This is done in such a way as
    /// to reduce garbage as much as possibly using a circular buffer of
    /// task items.
    /// </summary>
    internal sealed class TaskQueue : IDisposable
    {
        /// <summary>
        /// An event that is set when there are tasks to be processed on the queue.
        /// </summary>
        private ManualResetEvent dataReadyEvent = new ManualResetEvent(false);

        /// <summary>
        /// Used to maintain the queue state when called by multiple threads.
        /// </summary>
        private object threadLock = new object();

        /// <summary>
        /// The insertion point in the circular buffer of the
        /// next task.
        /// </summary>
        private int nextAddIndex = 0;

        /// <summary>
        /// The index of the next task in the circular buffer to execute.
        /// </summary>
        private int nextExecuteIndex = 0;

        /// <summary>
        /// The circular buffer of task items.
        /// </summary>
        private TaskItem[] items;

        /// <summary>
        /// Instantiates a new instance of the TaskQueue class.
        /// </summary>
        /// <param name="length">The size of the buffer.</param>
        public TaskQueue(int length)
        {
            // create the buffer
            this.items = new TaskItem[length];

            for (int i = 0; i < length; i++)
            {
                this.items[i] = new TaskItem();
            }
        }

        /// <summary>
        /// Cleans up any contained disposable resources.
        /// </summary>
        public void Dispose()
        {
            if (this.dataReadyEvent != null)
            {
                this.dataReadyEvent.Close();
                this.dataReadyEvent = null;
            }
        }

        /// <summary>
        /// Gets the number of unrocessed items in the queue.
        /// </summary>
        public int UnprocessedItemsCount
        {
            get
            {
                int rv = 0;

                // loop through the buffer looking for items that are either
                // executing or queuing
                for (int i = 0; i < this.items.Length; i++)
                {
                    if (this.items[i].ItemState != TaskItemState.NotInUse)
                    {
                        rv++;
                    }
                }

                return rv;
            }
        }

        /// <summary>
        /// Gets the number of unrocessed items in the queue.
        /// </summary>
        public int UnreadItemsCount
        {
            get
            {
                int rv = 0;

                // loop through the buffer looking for items that are either
                // executing or queuing
                for (int i = 0; i < this.items.Length; i++)
                {
                    if (this.items[i].ItemState == TaskItemState.Queuing)
                    {
                        rv++;
                    }
                }

                return rv;
            }
        }

        /// <summary>
        /// Gets the event handle that indicates when there are new items
        /// to process.
        /// </summary>
        public WaitHandle DataAvailableEvent
        {
            get { return this.dataReadyEvent; }
        }

        /// <summary>
        /// Pushes a new item of work onto the queue.
        /// </summary>
        /// <param name="callback">
        /// A reference to a delegate that is used 
        /// to populate the item information.</param>
        public void PushItem(TaskCallback task, object param)
        {
            // the callback must be set in the task
            if (task == null)
            {
                throw new InvalidOperationException(@"The Callback delegate in the task item must be set.");
            }

            lock (this.threadLock)
            {
                // check to make sure we have space
                if (this.items[this.nextAddIndex].ItemState != TaskItemState.NotInUse)
                {
                    throw new IndexOutOfRangeException(@"Cannot add another task, queue too short.");
                }

                // make sure whatever was in the buffer at this location
                // before is zeroed out
                this.items[this.nextAddIndex].Reset();

                // set up the paramters for the call
                this.items[this.nextAddIndex].Callback = task;
                this.items[this.nextAddIndex].Parameter = param;

                // set the item to be waiting for some work
                this.items[this.nextAddIndex].ItemState = TaskItemState.Queuing;

                // move the insert point
                this.nextAddIndex++;

                // wrap around if necessary
                if (this.nextAddIndex == this.items.Length)
                {
                    this.nextAddIndex = 0;
                }

                // a new item has been added
                this.dataReadyEvent.Set();
            }
        }

        /// <summary>
        /// Gets the next item off the queue.
        /// </summary>
        /// <returns>A reference to the item.</returns>
        public TaskItem PopItem()
        {
            TaskItem rv = null;

            lock (this.threadLock)
            {
                // check that there is something for us to do
                if (this.items[this.nextExecuteIndex].ItemState == TaskItemState.Queuing)
                {
                    // get a reference to the next task to execute
                    rv = this.items[this.nextExecuteIndex];

                    // put the task in the executing state
                    rv.ItemState = TaskItemState.Processing;

                    // move the index on to the next item
                    this.nextExecuteIndex++;

                    // wrap around if required
                    if (this.nextExecuteIndex == this.items.Length)
                    {
                        this.nextExecuteIndex = 0;
                    }

                    // if there is nothing left in the queue then
                    // reset the event
                    if (this.UnreadItemsCount == 0)
                    {
                        this.dataReadyEvent.Reset();
                    }
                }
            }

            return rv;
        }
    }
}
